<div id="app"></div>

<script>
  // vdom

  // 渲染函数 直接返回一个简单的 vNode
  function h(tag, props, children) {
    return {
      tag,
      props,
      children
    }
  }

  // 将 vNode 挂载到真实的 DOM 上面
  function mount(vnode, container) {
    console.log(vnode, 'vnode')
    // 根据 vNode 信息创建 DOM
    const el = vnode.el = document.createElement(vnode.tag)

    // props
    if (vnode.props) {
      for (const key in vnode.props) {
        const value = vnode.props[key];
        // 设置 DOM 的属性
        if (key.startsWith('on')) {
          el.addEventListener(key.slice(2).toLowerCase(), value)
        } else {
          el.setAttribute(key, value)
        }
      }
    }
    // children
    if (vnode.children) {
      // 如果是字符串，直接修改 textContent
      if (typeof vnode.children === 'string') {
        el.textContent = vnode.children
      } else {
        // 数组里面默认都是 vNode
        vnode.children.forEach(child => {
          mount(child, el)
        })
      }
    }

    // 将创建的 DOM 元素放到 container 中
    container.appendChild(el)
  }

  // patch 阶段
  function patch(n1, n2) {
    // 如果两个 node 是相同类型的
    if (n1.tag === n2.tag) {
      // 拿到旧的节点的 DOM
      const el = n2.el = n1.el
      // props
      const oldProps = n1.props || {}
      const newProps = n2.props || {}
      // 遍历新的属性，如果和旧的属性不相等，那么就设置新的属性
      for (const key in newProps) {
        const oldValue = oldProps[key]
        const newValue = newProps[key]
        if (newValue !== oldValue) {
          el.setAttribute(key, newValue)
        }
      }
      // 遍历旧的属性，如果发现新的属性没有这项了，则删除旧的属性
      for (const key in oldProps) {
        if (!(key in newProps)) {
          el.removeAttribute(key)
        }
      }

      // children
      const oldChildren = n1.children
      const newChildren = n2.children
      if (typeof newChildren === 'string') {
        if (typeof oldChildren === 'string') {
          // 新的孩子和旧的孩子都是 string 类型，即都是文本类型
          if (newChildren !== oldChildren) {
            // 直接替换 textContent
            el.textContent = newChildren
          }
        } else {
          // 新孩子是文本节点，但是旧孩子是数组，直接设置 textContent，
          // 这样会覆盖旧的子 DOM 节点，并丢弃它们
          el.textContent = newChildren
        }
      } else {
        if (typeof oldChildren === 'string') {
          // 新孩子是数组，但是旧孩子是文本节点
          // 清空旧的文本节点
          el.innerText = ''
          // 遍历数组，将子 vNode 挂载到 el 上
          newChildren.forEach(child => {
            mount(child, el)
          })
        } else {
          // 新孩子和旧孩子都为数组
          // diff 阶段
          // diff 在 Vue 内部有两种方式，一种是通过 key，一种是遍历比较
          // 这里直接遍历
          // 获取共有的长度，直接每个子节点都进行 patch
          // 这样可能非常低效，但是足够直观并且能保证一致性
          const commonLength = Math.min(oldChildren.length, newChildren.length)
          for (let i = 0; i < commonLength; i++) {
            patch(oldChildren[i], newChildren[i])
          }
          if (newChildren.length > oldChildren.length) {
            // 如果新孩子更长，将新孩子多余的节点添加到 DOM 上
            newChildren.slice(oldChildren.length).forEach(child => {
              mount(child, el)
            })
          } else if (newChildren.length < oldChildren.length) {
            // 如果旧孩子更长，将旧孩子多余的节点从 DOM 上移除
            oldChildren.slice(newChildren.length).forEach(child => {
              el.removeChild(child.el)
            })
          }
        }
      }

    } else {
      // 如果两个 node 是不同类型的，则需要用新的 node 替换 旧的 node
      // 这里省略
      // replace
    }
  }

  // reactivity

  let activeEffect;

  class Dep {
    subscribers = new Set();

    depend() {
      if (activeEffect) {
        this.subscribers.add(activeEffect);
      }
    }

    notify() {
      this.subscribers.forEach(effect => {
        effect()
      });
    }
  }

  function watchEffect(effect) {
    activeEffect = effect;
    effect();
    activeEffect = null;
  }

  const targetMap = new WeakMap();

  function getDep(target, key) {
    // 获取 target 对应的 deps，不存在就创建
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }
    // 获取 target[key] 对应的 dep，不存在就创建
    let dep = depsMap.get(key);
    if (!dep) {
      dep = new Dep();
      depsMap.set(key, dep);
    }
    return dep
  }

  const reactiveHandlers = {
    // 因为 get 和 set 里都需要获取 dep，故抽成一个获取 dep 的函数
    get(target, key, receiver) {
      const dep = getDep(target, key);
      // 收集依赖
      dep.depend();
      // 使用 Reflect 确保与原始的 get 一致
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const dep = getDep(target, key);
      // Proxy 的 set 需要返回一个值
      const result = Reflect.set(target, key, value, receiver);
      // 通知更新
      dep.notify();
      return result;
    },
  }

  function reactive(raw) {
    // 使用 Proxy，更方便地拦截处理
    return new Proxy(raw, reactiveHandlers);
  }

  // App 组件
  const App = {
    data: reactive({
      count: 0
    }),
    render() {
      return h('div', {
        // 这里需要在 mount 中添加事件处理，之前没有实现
        onClick: () => {
          this.data.count++
        }
      }, String(this.data.count)) // 第三个参数这里只支持 String
    }
  }

  // 挂载 App
  function mountApp(component, container) {
    let isMounted = false
    let prevVdom
    // component 组件中有响应式对象发生变化，便会执行以下函数
    watchEffect(() => {
      if (!isMounted) {
        // 没有挂载，即初始化
        // 记录旧的 vdom
        prevVdom = component.render()
        // 挂载
        mount(prevVdom, container)
        isMounted = true
      } else {
        // 获取新的 vdom
        const newVdom = component.render()
        // patch
        patch(prevVdom, newVdom)
        prevVdom = newVdom
      }
    })
  }

  mountApp(App, document.getElementById('app'))

</script>